<?php
/**
 * 生成手机校验码并发送，对应接口为/common/gen_mobile_token
 * User: devforma
 * Date: 14/12/12
 * Time: 12:35
 */

require_once LIB_DIR.'/sms.class.php';
require LIB_DIR.'/phpass.class.php';
require_once LIB_DIR . '/class.phpmailer.php';
require_once LIB_DIR . '/class.smtp.php';

class GenMobileToken implements iController{
    private static $smsConfig;  //短信平台账号配置
    private $appType;
    private $mobile;

    function __construct() {
        self::$smsConfig = require_once CONFIG_DIR.'/sms.config.php';
        $this->appType = array(
            'dailyplan' => '上岸计划',
            'quizbank' => '天天模考'
        );
    }

    /**
     * api请求的逻辑处理
     * @param array $queryArgs 请求参数
     * @return array 返回结果
     */
    public function output($queryArgs) {
        global $uddb;

        if (!empty($queryArgs['terminal_type']) && $queryArgs['terminal_type'] == 'mobile_web') {
            $queryArgs['mobile_num'] = passDecrypt($queryArgs['mobile_num'], ENCRYPTION_KEY);
        }

        // action=reg表示手机号注册动作，action=update表示更新手机号，action=bind表示绑手机号
        if (!isset($queryArgs['mobile_num']) || !validatePhone($queryArgs['mobile_num'])) {
            return genOutput(1000); //手机号无效
        }

//        $this->mobile = $queryArgs['mobile_num'];
//        $ipAddr = array_key_exists('client_ip', $queryArgs) ? $queryArgs['client_ip'] : $_SERVER['REMOTE_ADDR'];
//        if ( $this->isRateLimit($ipAddr, $_SERVER['HTTP_USER_AGENT']) ) {
//            return genOutput(403);
//        }

        //是否为语音验证码
        $isVoice = !empty($queryArgs['voice']) && $queryArgs['voice'] == 1;

        //TODO 这个接口action=trigger的时候，不管手机号是否存在，都会给手机号发送验证码
        //TODO 这个逻辑在现有代码之下就是成立的，所以代码上没有变动，如果以后需要更改这部分代码，需要知道这个事情。
        //判断该手机号是否已存在
        $phoneExists = $uddb->getVar("SELECT `ID` FROM `{$uddb->userAuthInfo}` WHERE `phone`=:phone",
            array(':phone' => $queryArgs['mobile_num']));
        
        //如果手机号已存在，且不是重置密码的情况，则非法
        if (empty($queryArgs['action']) && !empty($phoneExists)) {
            return genOutput(1102);
        } elseif (!empty($queryArgs['action']) && $queryArgs['action'] == 'resetPswd' && empty($phoneExists)) {
            //重置密码但手机号不存在，则非法
            return genOutput(1000);
        }

        $memKey = 'phone:'.$queryArgs['mobile_num'];

        $sms = new SmsSender(self::$smsConfig);

        if ($value = cacheGet($memKey)) {	//已经发过校验码
            if (time() - $value['timestamp'] < 58) {	 //距离上次发送校验码后60秒内的请求，非法
                return genOutput(1104); //操作过于频繁

            } else {	//距离上次发送校验码超过60秒，合法
                $regCode = $value['code']; //使用之前的验证码
                $smsContent = str_replace(array('#app#', '#code#'), array($this->appType[$queryArgs['app_type']], $regCode),
                        self::$smsConfig['sms_content']).self::$smsConfig['sms_sign'];

                if($value['platform'] == 'yx') { //短信平台一次一换
                    $result = $sms->sendMsg('lm', $queryArgs['mobile_num'], $smsContent, $isVoice);
                    if ($result) {
                        $value['repeat']++;
                        $value['timestamp'] = time();
                        $value['code'] = $regCode;
                        $value['platform'] = 'lm';
                        $value['validated'] = false;
                        cacheSet($memKey, $value, 300);

                        return genOutput(1); //成功
                    } else {
                        return genOutput(1105);  //发送失败
                    }

                } else {
                    //对接平台
                    $result = $sms->sendMsg('yunxin', $queryArgs['mobile_num'], $smsContent, $isVoice);
                    if ($result) {
                        $value['repeat']++;
                        $value['timestamp'] = time();
                        $value['code'] = $regCode;
                        $value['platform'] = 'yx';
                        $value['validated'] = false;
                        cacheSet($memKey, $value, 300);

                        return genOutput(1); //成功
                    } else {
                        return genOutput(1105); //发送失败
                    }
                }

            }

        } else {	//第一次请求校验码或者合法的重新发送校验码

            $regCode = self::generateRegCode();
            $smsContent = str_replace(array('#code#'), array($regCode),
                    self::$smsConfig['sms_content']).self::$smsConfig['sms_sign'];
            //对接平台
            $result = $sms->sendMsg('lm', $queryArgs['mobile_num'], $smsContent, $isVoice);
            if ($result) {
                $value['repeat'] = 1;
                $value['platform'] = 'lm';
                $value['code'] = $regCode;
                $value['timestamp'] = time();
                $value['validated'] = false;

                if (!empty($queryArgs['action']) && $queryArgs['action'] == 'token_login')
                    $value['token_login'] = true;

                cacheSet($memKey, $value, 300);

                return genOutput(1); //成功
            } else {
                return genOutput(1105); //发送失败
            }
        }
    }

    /**
     * 检测请求限制
     *
     * @param string $cacheKey 缓存Key
     * @param array $args 请求控制的边界参数
     * (
     *  'expires', // 正常计数的过期时间
     *  'freeze_time', // 请求冻结时间
     *  'period', // 单次监控周期
     *  'max_retries', // 周期内允许的最大尝试次数
     *  'interval' // 两次请求允许的最小时间间隔
     * )
     * @param string $flag 检测标识
     *
     * @return bool
     */
    private function checkRateLimit($cacheKey, $args, &$flag) {
        if ($data = cacheGet($cacheKey)) {
            // 处于封禁状态
            if (array_key_exists('limited', $data)) {
                //if (!$data['sendmail'] && $this->sendReport($cacheKey, $flag)) {
                if (!$data['sendmail']) {
                    $data['sendmail'] = true;
                    cacheSet($cacheKey, $data, $args['freeze_time']);
                    $this->log($cacheKey, $flag);
                }
                return true;
            }

            $curTime = time();

            // 在指定时间周期内发送超4次，封禁半小时
            if (($curTime - $data['first'] <= $args['period']) && $data['times'] == $args['max_retries']) {
                $data = array('limited' => true, 'sendmail' => false);
                cacheSet($cacheKey, $data, $args['freeze_time']);
                return true;
            }

            $data['times'] += 1;
            $lastTime = $data['last'];
            $data['last'] = $curTime;

            // 间隔大于指定值,返回true
            if (($curTime - $lastTime) >= $args['interval']) {
                cacheSet($cacheKey, $data, $args['expires']);
                return false;
            } else {
                // 间隔小于指定值自动计数
                cacheSet($cacheKey, $data, $args['expires']);
            }

            return true;
        }

        $data = array('first' => time(), 'last' => time(), 'times' => 1);
        cacheAdd($cacheKey, $data, $args['expires']);

        return false;
    }

    /**
     * 基于IP和客户端进行请求限制的检测
     *
     * @param string $ip 源IP
     * @param string $userAgent 客户端
     * @return bool
     */
    private function isRateLimit($ip, $userAgent) {
        // IPv4内网,不做计数
        $parts = explode('.', $ip);
        if (count($parts) == 4 && $parts[0] == '10') return false;

        $args = array(
            'expires' => 120, // 正常计数的过期时间
            'freeze_time' => 1800,  // 请求冻结时间
            'period' => 120, // 单次监控周期
            'max_retries' => 4, // 周期内允许的最大尝试次数
            'interval' => 30 // 两次请求允许的最小时间间隔
        );

        if ($this->checkRateLimit('rl_' . $ip, $args, $ip)) return true;

        $args = array(
            'expires' => 60,
            'freeze_time' => 60 * 60,
            'period' => 60,
            'max_retries' => 60,
            'interval' => 1
        );

        return $this->checkRateLimit(md5($userAgent), $args, $userAgent);
    }

    /**
     * 发送报告
     *
     * @param string $cacheKey
     * @param string $flag
     * @return bool
     */
    private function sendReport($cacheKey, $flag) {
        $text = '触发时间: ' . date('Y-m-d H:i:s') . "<br /><br />";
        $text .= 'User-Agent: ' . $flag . "<br /><br />";
        $text .= 'Cache-Key: ' . $cacheKey . "<br /><br />";
        $text .= 'Phone: ' . $this->mobile;

        return sendMail($text, '[警告] API短信接口触发限制');
    }

    protected function log($cacheKey, $flag) {
        Logger::i($flag . ' - ' . $cacheKey . ' - ' . $this->mobile);
    }

    /**
     * 生成6位数字校验码
     * @return string 6位数字校验码
     */
    private static function generateRegCode() {
        return rand(0, 9).rand(0, 9).rand(0, 9).rand(0, 9).rand(0, 9).rand(0, 9);
    }
}
